package com.ztr.excel.utils;

import cn.hutool.core.io.FileUtil;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;

import com.ztr.excel.Constants;
import com.ztr.excel.enums.ExcelExportFileTypeEnum;
import com.ztr.excel.enums.ExcelImportFileTypeEnum;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.jxls.common.Context;
import org.jxls.reader.ReaderBuilder;
import org.jxls.reader.XLSReadStatus;
import org.jxls.reader.XLSReader;
import org.jxls.transform.poi.PoiTransformer;
import org.jxls.util.JxlsHelper;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;

/**
 * <p>
 * 导入导出Excel报表工具类
 * </p>
 */
@Slf4j
public class ExcelUtil {


    /**
     * 从Excel文件中读取数据并将其转换为指定类型的列表。
     *
     * @param dataList                用于存储读取后转换的数据的列表。
     * @param excelImportFileTypeEnum Excel文件的类型枚举，用于确定解析策略。
     * @param file                    需要读取的Excel文件。
     * @param isThrowException        当读取过程中发生错误时，是否抛出异常。
     * @param <E>                     数据实体的类型。
     * @param <T>                     数据列表的元素类型。
     * @return 更新后的数据列表。
     * @throws Exception 如果isThrowException为true且读取过程中发生错误，则抛出异常。
     */
    @SneakyThrows(Exception.class)
    public static <E, T> List<T> read(List<T> dataList, ExcelImportFileTypeEnum excelImportFileTypeEnum,
                                      MultipartFile file, boolean isThrowException) {
        String fileName = file.getName();
        InputStream inputXLS = null;
        InputStream inputXML = null;
        try {
            // 加载XML配置文件，该文件定义了Excel表格与数据对象之间的映射关系。
            Resource resource =
                    new ClassPathResource(Constants.DEFAULT_REPORT_IMPORT_FOLDER + excelImportFileTypeEnum.getMappingXml());
            // 获取Excel文件的输入流。
            // 上传文件流
            inputXLS = file.getInputStream();
            // 获取XML配置文件的输入流。
            inputXML = resource.getInputStream();
            // 根据XML配置构建读取器。
            XLSReader mainReader = ReaderBuilder.buildFromXML(inputXML);
            // 准备数据对象映射。
            Map<String, Object> beans = new HashMap<>(1);
            beans.put("dataList", dataList);
            // 执行Excel数据的读取和转换。
            XLSReadStatus readStatus = mainReader.read(inputXLS, beans);
            // 如果读取和转换成功，记录日志。
            if (readStatus.isStatusOK()) {
                log.debug("读取excel文件成功: 【{}】", fileName);
            }
        } catch (Exception e) {
            // 解析错误处理：定位错误单元格，确定错误原因，并根据错误类型转换为用户友好的错误信息。
            // ① 记录错误位置
            String errorCell = e.getMessage().split(" ")[3];
            // ② 记录错误原因
            String errorMsg = e.getCause().toString();
            String[] causeMsgArray = errorMsg.split(":");
            errorMsg = errorMsg.substring(causeMsgArray[0].length() + 2).split(":")[0];
            // 根据错误原因转换为用户友好的错误信息。
            switch (errorMsg) {
                case "For input string":
                    errorMsg = "时间格式不正确";
                    break;
                case "Error converting from 'String' to 'Integer' For input string":
                    errorMsg = "请填写数字类型";
                    break;
                default:
                    break;
            }
            // 构造错误信息，包括文件名、错误单元格和错误原因。
            errorMsg = "读取" + fileName + "文件异常: " + errorCell + errorMsg;
            // 根据isThrowException参数决定是抛出异常还是仅记录错误日志。
            if (isThrowException) {
                throw new Exception(errorMsg);
            } else {
                log.error(errorMsg);
            }
        } finally {
            // 关闭输入流，确保资源释放。
            try {
                if (inputXLS != null) {
                    inputXLS.close();
                }
                if (inputXML != null) {
                    inputXML.close();
                }
            } catch (IOException e) {
                // 记录关闭输入流过程中发生的任何异常。
                log.error("parse excel error : 【{}】", e.getMessage());
            }
        }
        // 返回处理后的数据列表。
        return dataList;
    }

    /**
     * 导出数据到Excel文件。
     *
     * @param dataList                数据列表，每个元素是一个包含导出数据的Map。
     * @param excelExportFileTypeEnum Excel导出文件的类型枚举，定义了导出文件的格式和表单名。
     * @param exportPath              导出文件保存的路径。
     * @return 导出文件的绝对路径。
     * @throws Exception 如果导出过程中发生错误，会抛出异常。
     */
    @SneakyThrows(Exception.class)
    public static String export(List<Map<String, Object>> dataList, ExcelExportFileTypeEnum excelExportFileTypeEnum,
                                String exportPath) {
        // 处理数据导出，根据传入的数据列表、导出文件类型和路径生成导出文件。
        // 处理导出
        File exportFile = handleExport(dataList, excelExportFileTypeEnum, exportPath);
        // 根据导出文件类型枚举生成导出文件名。
        String fileName = excelExportFileTypeEnum.getSheetName() + ".xls";
        // TODO: 这里可以添加对导出文件的进一步处理，比如文件上传、邮件发送等。
        // TODO 这里可以对`exportFile`做文件上传处理，然后返回一个文件下载地址 或其它业务处理...
        return exportFile.getAbsolutePath(); // 返回导出文件的绝对路径。
    }


    /**
     * 导出数据到Excel文件，并通过Http响应返回给前端。
     * 使用了SneakyThrows注解来静默处理异常，避免导出过程中由于异常导致的中断。
     *
     * @param dataList                需要导出的数据列表，每个元素是一个包含导出数据的Map。
     * @param excelExportFileTypeEnum 导出的Excel文件类型枚举，用于确定导出的文件格式。
     * @param exportPath              生成的Excel文件临时存储路径。
     * @param response                Http响应对象，用于将生成的Excel文件作为响应返回给前端。
     * @throws Exception 如果导出过程中发生错误，会被SneakyThrows注解静默处理。
     */
    @SneakyThrows(Exception.class)
    public static void export(List<Map<String, Object>> dataList, ExcelExportFileTypeEnum excelExportFileTypeEnum,
                              String exportPath, HttpServletResponse response) {
        // 处理数据导出逻辑
        // 处理导出
        handleExport(dataList, excelExportFileTypeEnum, exportPath);

        // 设置Http响应，以便将生成的Excel文件作为附件下载返回给前端
        // ======================= ↓↓↓↓↓↓ 响应给前端 ↓↓↓↓↓↓ =======================
        // 文件名 - 解决中文乱码问题
        String filename = URLEncoder.encode(excelExportFileTypeEnum.getTemplateFile().substring(1), "UTF-8");
        // 设置响应编码
        response.setCharacterEncoding("UTF-8");
        // 设置响应类型为下载文件
        response.setContentType("application/x-download");
        // 设置响应头，指定文件名和下载方式
        response.setHeader("Content-Disposition", "attachment;filename=" + filename);

        // 获取响应输出流
        OutputStream outputStream = response.getOutputStream();
        // 创建文件输入流，准备读取生成的Excel文件
        InputStream inputStream = new FileInputStream(exportPath);
        // 定义缓冲区，用于读取和写入数据
        byte[] buffer = new byte[1024];
        int i = -1;
        // 通过循环将文件内容写入响应输出流
        while ((i = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, i);
        }
        // 刷新输出流，确保所有数据都被写入
        outputStream.flush();
        // 关闭输出流和输入流
        outputStream.close();
        inputStream.close();
    }


    /**
     * 处理导出操作，根据给定的数据列表、导出文件类型和导出路径，生成Excel文件。
     * 使用Jxls库和给定的模板文件，将数据填充到Excel模板中，然后保存为指定的导出文件。
     *
     * @param dataList                数据列表，包含要导出的信息。
     * @param excelExportFileTypeEnum 导出文件的类型枚举，用于确定使用哪个模板文件。
     * @param exportPath              导出文件的保存路径。
     * @return 返回创建的导出文件。
     * @throws Exception 如果处理过程中发生任何错误，将抛出异常。
     */
    @SneakyThrows(Exception.class)
    private static File handleExport(List<Map<String, Object>> dataList,
                                     ExcelExportFileTypeEnum excelExportFileTypeEnum, String exportPath) {
        // 通过枚举获取模板文件路径并加载模板文件
        Resource resource =
                new ClassPathResource(Constants.DEFAULT_REPORT_EXPORT_FOLDER + excelExportFileTypeEnum.getTemplateFile());
        InputStream templateInputStream = resource.getInputStream();

        // 日志记录导出文件的路径
        log.debug("导出文件地址为:{}", exportPath);

        // 创建或更新导出文件
        // 创建文件
        File exportFile = FileUtil.touch(exportPath);

        // 打开导出文件的输出流
        // 列表数据将存储到指定的excel文件路径
        OutputStream out = new FileOutputStream(exportFile);

        // 初始化Jxls上下文
        // 这里的context是jxls框架上的context内容
        Context context = PoiTransformer.createInitialContext();
        // 将数据列表填充到上下文中
        // 将列表参数放入context中
        context.putVar("dataList", dataList);

        // 根据模板输入流创建工作簿
        Workbook workbook = WorkbookFactory.create(templateInputStream);
        // 修改第一个工作表的名称
        // Changing name of the first sheet
        workbook.setSheetName(0, excelExportFileTypeEnum.getSheetName());

        // 创建转换器，并设置输出流
        PoiTransformer transformer = PoiTransformer.createTransformer(workbook);
        transformer.setOutputStream(out);

        // 使用Jxls处理模板和上下文，将数据填充到模板中
        // 将列表数据按照模板文件中的格式生成
        JxlsHelper.getInstance().processTemplate(context, transformer);

        // 关闭输入输出流
        templateInputStream.close();
        out.close();

        // 返回生成的导出文件
        return exportFile;
    }


}
